/*  Copyright (C) 2008  Alexander Blyzniuchenko (BlezAlex)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

using System;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Text.RegularExpressions;

namespace DC
{ 
    public class HubClient : BaseDcClient
    {
        private const string portDelimiter = ":";

        private readonly string _nick;
        private readonly string _myIp;
        private TcpClient toServerClient;
        private readonly int _ID;
        private readonly Socket _updSocket;
        private readonly DownloadManager _downloadManager;
        private bool _isConnected;
        private readonly DcDirectory _directory = new DcDirectory("UnKnown");
        private string _name;
        
        private readonly static Regex _searchReg =
            new Regex(@"\$SR (?<nick>\S+)\s(?<path>[^\x05]+)\x05(?<size>\d+)\s(?<current>\d+)/(?<total>\d+)\x05TTH:(?<TTH>\S+)", 
                RegexOptions.IgnoreCase | RegexOptions.Compiled);

        private readonly static Regex _myInfoReg = 
            new Regex(@"\$MyINFO\s\$ALL\s(?<nick>\S+)\s(?<desc>[^<\$]*)<{0,1}(?<cinfo>[^>]*)>{0,1}\$\s\$" + 
                      @"(?<contype>[^\x03\x09\x01\x0b\x05]*)[\x03\x09\x01\x0b\x05]*\s*\$(?<email>[^$]*)\$(?<shared>\d+)\$",
                RegexOptions.Compiled);

       

        public delegate void StringArrayEvent(string[] nicks);
        public event EventHandler<StringEventArgs> OnConnected;
        public event EventHandler<StringEventArgs> OnChatMessage;
        public event StringArrayEvent OnNickList;
        public event StringArrayEvent OnOPList;
        public event EventHandler<UserIpListEventArgs> OnUserIPList;
        public event EventHandler<FoundFileEventArgs> OnSearch;
        public event EventHandler<StringEventArgs> OnError;
        public event EventHandler<StringEventArgs> OnUserConncected;
        public event EventHandler<StringEventArgs> OnUserLeave;
        public event EventHandler<MyInfoEventArgs> OnMyInfo;
        public event EventHandler<FileRecivedEventArgs> OnFileRecived;
        public event EventHandler<FileListRecivedEventArgs> OnFileListRecived;

        internal HubClient(string nick, string myIp, int ID, string tempFolder)
        { 
            _ID = ID;
            _nick = "[MDC]" + nick;
            _myIp = myIp;
            _updSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
            _updSocket.Bind(new IPEndPoint(IPAddress.Any, 0));
            StartReadUdpSocket();

            _downloadManager = new DownloadManager(this, tempFolder);
            _downloadManager.OnFileRecived += DownloadManager_OnFileRecived;
            _downloadManager.OnFileListRecived += DownloadManager_OnFileListRecived;
        }

        public void Connect(string connectionString)
        {
            try
            {
                _name = connectionString.Replace(':', '_');
                _directory.Name = _name;
                string host = connectionString;
                int port = 411;
                if (connectionString.Contains(portDelimiter))
                {
                    host = connectionString.Substring(0, connectionString.IndexOf(portDelimiter));
                    port = int.Parse(connectionString.Substring(connectionString.IndexOf(portDelimiter) + 1));
                }
                toServerClient = new TcpClient();
                toServerClient.BeginConnect(host, port, ConnectCallback, null);
            }
            catch(Exception ex)
            {
                if (OnError != null)
                    OnError(this, new StringEventArgs(ex.Message));
            }
        }

        public void SendToChat(string text)
        {
            SendCommand("<" + _nick + "> " + text);
        }

        public void SendPM(string to, string message)
        {
            SendCommand("$To: " + to + " From: " + _nick + " $<" + _nick + "> " + message);
        }

        public void Search(bool isSizeRestricted, bool isMinimumSize, long size, DcDataType type, string pattern)
        {
            //$Search <clientip>:<clientport> <searchstring> <sizerestricted>?<isminimumsize>?<size>?<datatype>?<searchpattern> <sizerestricted>
            StringBuilder sb = new StringBuilder();
            sb.Append((isSizeRestricted ? "T" : "F") + "?");
            sb.Append((isMinimumSize ? "T" : "F") + "?");
            sb.AppendFormat("{0}?", isSizeRestricted ? size : 0);
            sb.AppendFormat("{0}?", ((int) type));
            if (type == DcDataType.TTH)
                sb.Append("TTH:");
            sb.Append(pattern.Replace(' ', '$'));
            SendCommand(String.Format("$Search {0}:{1} {2}", _myIp, ((IPEndPoint)_updSocket.LocalEndPoint).Port, sb));
        }

        public void Close()
        {
            toServerClient.Close();
        }
        
        internal string MyNick
        {
            get { return _nick; }
        }

        public void RequestFileList(string nickName)
        {
            _downloadManager.RequestFileList(nickName);
        }

        public void RequestFile(string nickName, string fileName, string TTH)
        {
            _downloadManager.RequestFile(nickName, fileName, TTH);
        }

        public DCStream GetFile(string path, long startPos, long endPos)
        {
            return _downloadManager.GetFile(path, startPos, endPos);
        }

        public int ID
        {
            get { return _ID; }
        }

        public bool IsConnected
        {
            get { return _isConnected; }
        }

        public DcDirectory Directory
        {
            get { return _directory; }
        }

        internal DownloadManager DownloadManager
        {
            get { return _downloadManager; }
        }

        public string Name
        {
            get { return _name; }
        }


        internal void ConnectToMe(string to, int port)
        {
            SendCommand(String.Format("$ConnectToMe {0} {1}:{2}", to, _myIp, port));
        }



        protected internal override CommandType GetCommandType(string command)
        {
            CommandType result = base.GetCommandType(command);
            if (result == CommandType.Unknow)
            {
                if (command.StartsWith("$Hello "))
                    return CommandType.Hello;

                if (command.StartsWith("$Quit "))
                    return CommandType.Quit;

                if (command.StartsWith("$NickList "))
                    return CommandType.NickList;

                if (command.StartsWith("$Search "))
                    return CommandType.Search;

                if (command.StartsWith("$MyINFO "))
                    return CommandType.MyInfo;

                if (command.StartsWith("$OpList "))
                    return CommandType.OpList;

                if (command.StartsWith("$UserIP "))
                    return CommandType.UserIP;
                
                if (command[0] == '<')
                    return CommandType.Chat;
            }
            return result;
        }

        protected override bool ProcessCommand(string command)
        {
            switch (GetCommandType(command))
            {
                case CommandType.Lock:
                    SendCommand("$Supports NoGetINFO UserIP2 TTHSearch GetZBlock ");
                    ProcessLockCommand(command);
                    SendCommand("$ValidateNick" + " " + _nick);
                    break;

                case CommandType.Hello:
                    {
                        string arrivedNick = command.Substring("$Hello ".Length).Trim();
						if (arrivedNick == _nick)
						{
                            string myVersion = "MediaDC.Net++ " + System.Reflection.Assembly.GetEntryAssembly().GetName().Version;
                            SendCommand("$Version " + myVersion);
                            SendCommand(String.Format("$MyINFO $ALL {0} {1}<{3},M:A,H:5/0/0,S:10>$ $1$${2}$", _nick, "MediaDC.Net+Just+See+It", 85965476073, myVersion));
							SendCommand("$GetNickList");
						}
                        else
						{
                            _directory.Directories.Add(new FileListing(arrivedNick, false, this));
                            if (OnUserConncected != null)
                                OnUserConncected(this, new StringEventArgs(arrivedNick));
						}
                    }
                    break;

                case CommandType.Quit:
                    if (OnUserLeave != null)
                    {
                        string nick = command.Substring("$Quit ".Length).Trim();
                        _directory.Directories.Remove(new FileListing(nick, false, this));
                        OnUserLeave(this, new StringEventArgs(nick));
                    }
                    break;

                case CommandType.Chat:
                    if (OnChatMessage != null)
                    {
                        OnChatMessage(this, new StringEventArgs(command.Replace("\r", "").Replace("\n", Environment.NewLine)));
                    }
                    break;

                case CommandType.NickList:
                    {
                        if (OnNickList != null)
                        {
                            string[] nickList = command.Substring(9).Replace("<", "").Replace(">", "").Replace("$$", "$").Replace(" ", "").Split('$');
                            foreach (string nick in nickList)
                            {
                                if (string.IsNullOrEmpty(nick.Trim()))
                                    continue;
                                _directory.Directories.Add(new FileListing(nick, false, this));
                            }
                            OnNickList(nickList);
                        }
                        break;
                    }

                case CommandType.OpList:
                    {
                        if (OnOPList != null)
                        {
                            string[] opList = command.Substring(7).Replace("$$", "$").Replace(" ","").Split('$');
                            OnOPList(opList);
                        }
                        break;
                    }

                case CommandType.MyInfo:
                    {
                        if (OnMyInfo != null)
                        {
                            Match m = _myInfoReg.Match(command);
                            if (m.Success)
                                OnMyInfo(this, new MyInfoEventArgs(m.Groups["nick"].Value, 
                                                                    long.Parse(m.Groups["shared"].Value), 
                                                                    m.Groups["desc"].Value,
                                                                    m.Groups["cinfo"].Value,
                                                                    m.Groups["contype"].Value,
                                                                    m.Groups["email"].Value));
                        }

                        break;
                    }
                case CommandType.UserIP:
                    {
                        if (OnUserIPList != null)
                        {
                            UserIpListEventArgs args = new UserIpListEventArgs();
                            string[] ipListPairs = command.Substring(7).Replace("$$", "$").Split('$');
                            foreach (string pair in ipListPairs)
                                args.Load(pair);

                            OnUserIPList(this, args);   
                        }
                        break;
                    }

                //case CommandType.Search:
                //    {
                //        if (OnSearch != null)
                //        {
                //            Regex r = new Regex(@"\$Search\s(?<ip>\d+\.\d+\.\d+\.\d+):(?<port>\d+)\s(?<SearchString>\S*)", RegexOptions.IgnoreCase);
                //            Match m = r.Match(command);
                //            if (m.Success)
                //                OnSearch(m.Groups["ip"].Value,
                //                         int.Parse(m.Groups["port"].Value),
                //                         m.Groups["SearchString"].Value);
                //        }
                //    }
                //    break;
            }
            return true;
        }

        private void ConnectCallback(IAsyncResult result)
        {
            try
            {
                toServerClient.EndConnect(result);
                if (OnConnected != null)
                    OnConnected(this, new StringEventArgs(toServerClient.Client.RemoteEndPoint.ToString()));

                _netStream = toServerClient.GetStream();

                StartReadData();
                _isConnected = true;
            }
            catch (Exception ex)
            {
                if (OnError != null)
                    OnError(this, new StringEventArgs(ex.Message));
            }
        }

        private void DownloadManager_OnFileRecived(object sender, FileRecivedEventArgs e)
        {
            if (OnFileRecived != null)
                OnFileRecived(this, e);
        }

        void DownloadManager_OnFileListRecived(object sender, FileListRecivedEventArgs e)
        {
            if (OnFileListRecived != null)
                OnFileListRecived(this, e);
        }

        private void UdpMessageCallback(IAsyncResult result)
        {
            SearchReadState state = (SearchReadState)result.AsyncState;
            try
            {
                _updSocket.EndReceiveFrom(result, ref state.RemoteIP);
                StartReadUdpSocket();

                string command = _winEncoding.GetString(state.Buffer).Trim('\0');
                Debug.WriteLine(string.Format("FROM {0} Data: {1}", state.RemoteIP, command));

                Match m = _searchReg.Match(command);
                if (m.Success)
                {
                    DcFile file;
                    file.Name = Path.GetFileName(m.Groups["path"].Value);
                    file.Size = long.Parse(m.Groups["size"].Value);
                    file.TTH = m.Groups["TTH"].Value;

                    FoundFileEventArgs args =
                        new FoundFileEventArgs(file, m.Groups["path"].Value, m.Groups["nick"].Value, ID,
                                               int.Parse(m.Groups["current"].Value), int.Parse(m.Groups["total"].Value));

                    if (OnSearch != null)
                        OnSearch(this, args);
                }
            }
            catch (IOException ex)
            {
                Debug.WriteLine(ex);
            }
        }

        private void StartReadUdpSocket()
        {
            SearchReadState state = new SearchReadState();
            _updSocket.BeginReceiveFrom(state.Buffer, 0, SearchReadState.BUFFER_LENGTH, SocketFlags.None,
                                        ref state.RemoteIP, UdpMessageCallback, state);
        }
    }
}
